package com.soccertgc.comm
{
	import com.soccertgc.Application;
	import com.soccertgc.logging.Logger;
	import com.soccertgc.utils.JsonUtils;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.HTTPStatusEvent;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.net.URLVariables;

	public class Call extends EventDispatcher
	{
		
		// -------------------------------------------------------------------------------
		// CONSTANTS
		// -------------------------------------------------------------------------------	
		
		// Options
		protected static const		ERROR_RETRIES_IO:int					= 3;
		
		// Result values
		protected static const		RESULT_SUCCESS:int						=  0;
		protected static const		RESULT_ERROR:int						= -1;
		protected static const		RESULT_SERVER_EXCEPTION:int				= -2;
		
		// LIst of retriable HTPP_STATUSes that cause IO errors
		public static const			RETRY_HTTP_STATUS_LIST:Array			= [403];
		public static const			INVALID_HTTP_STATUS:uint				= 0;
		
		// JSON property definition
		protected static const		JSON_PROPERTY_RESULT:String				= "result";
		protected static const		JSON_PROPERTY_DATA:String				= "data";
		protected static const		JSON_PROPERTY_ERROR:String				= "error";
		protected static const		JSON_PROPERTY_ERROR_CODE:String			= "code";
		protected static const		JSON_PROPERTY_ERROR_MESSAGE:String		= "message";
		
		// Logging
		public static const			LOG_MARKER:String						= "CALL > ";
		public static const			LOG_MARKER_HTTP_STATUS:String			= "CALL > HTTP STATUS";
		public static const			DEBUG_KEY:String						= "comm.call";
		public static const			DEBUG_KEY_HTTP_STATUS:String			= "comm.call.httpstatus";
		
		// -------------------------------------------------------------------------------
		// STATIC
		// -------------------------------------------------------------------------------
		
		// Global handlers flag
		private static var _enableGlobalCallFailedHandling:Boolean			= true;
		private static var _enableGlobalErrorHandling:Boolean 				= true;
		
		// Global handler for all Call instances, invoked each time CallFailedEvent is dispatched 
		private static var _globalCallFailedHandler:Function;
		// Global handler for all Call instances, invoked each time CallInErrorEvent is dispatched
		private static var _globalCallErrorHandler:Function;
		
		// -------------------------------------------------------------------------------
		// FIELDS
		// -------------------------------------------------------------------------------
		
		private var _loader:URLLoader;
		// private var _retries:int;
		private var _httpStatus:int;
		private var _url:String;
		private var _params:Object;
		private var _data:Object;
		private var _error:CallError;
		private var _status:CallStatus;
		private var _useAutoResultHandling:Boolean;
		
		// -------------------------------------------------------------------------------
		// CONSTRUCTOR
		// -------------------------------------------------------------------------------
		
		public function Call(url:String, resultHandling:Boolean = false, params:Object = null, timeout:int = CallFactory.TIMEOUT)
		{
			// Initialization
			// _retries = 0;
			_url = url; // relative
			_params = params;
			_useAutoResultHandling = resultHandling;
			_status = CallStatus.READY;
			// Init objects
			_loader = new URLLoader();
			_loader.addEventListener(Event.OPEN, loaderOpen);
			_loader.addEventListener(Event.COMPLETE, loaderComplete);
			_loader.addEventListener(IOErrorEvent.IO_ERROR, loaderIOError);
			_loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loaderSecurityError);
			_loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, loaderHttpStatusChanged);
		}
		
		// -------------------------------------------------------------------------------
		// PROPERTIES
		// -------------------------------------------------------------------------------
		
		public function get url():String
		{
			return _url;
		}
		
		public function set url(value:String):void
		{
			_url = value;
		}
		
		public function get params():Object
		{
			return _params;
		}
		
		public function set params(value:Object):void
		{
			_params = value;
		}
		
		public function get data():Object
		{
			return _data;
		}
		
		public function get error():CallError
		{
			return _error;
		}
		
		public function get hasParams():Boolean
		{
			return _params != null;
		}
		
		public function get hasData():Boolean
		{
			return _data != null;
		}
		
		public function get useAutoResultHandling():Boolean
		{
			return _useAutoResultHandling;
		}
		
		public function set useAutoResultHandling(value:Boolean):void
		{
			_useAutoResultHandling = value;
		}

		public function get status():CallStatus
		{
			return _status;
		}

		public function get executing():Boolean
		{
			return	_status == CallStatus.EXECUTING;
		}		
		
		public function get executed():Boolean
		{
			return	_status == CallStatus.EXECUTED_WITH_SUCCESS ||
					_status == CallStatus.EXECUTED_WITH_ERROR;
		}
		
		public function get succeeded():Boolean
		{
			return	_status == CallStatus.EXECUTED_WITH_SUCCESS;
		}
		
		public function get failed():Boolean
		{
			return	_status == CallStatus.FAILED;
		}
		
		
		// -------------------------------------------------------------------------------
		// PRIVATE METHODS
		// -------------------------------------------------------------------------------
		
		protected function parseResponce(raw:Object):void
		{
			// Parsing error vars
			var formatError:Boolean = false;
			var formatErrorMessage:String = "";
			// Rasult value
			var result:int = RESULT_SERVER_EXCEPTION;
			var error:CallError = null;
			// Check for responce result
			if (raw.hasOwnProperty(JSON_PROPERTY_RESULT))
			{
				// Get result value
				result = int(raw[JSON_PROPERTY_RESULT]);
				// Check the result type
				switch(result)
				{
					case Call.RESULT_SUCCESS:
					{
						var data:Object = null;
						// Search for data
						if (raw.hasOwnProperty(JSON_PROPERTY_DATA))
						{
							data = raw[JSON_PROPERTY_DATA];
						}
						// Call > SUCCEDED
						callSucceeded(data);
						break;
					}
						
					case Call.RESULT_ERROR:
					{
						// Check for error structure
						if (raw.hasOwnProperty(JSON_PROPERTY_ERROR) && 
							raw[JSON_PROPERTY_ERROR].hasOwnProperty(JSON_PROPERTY_ERROR_CODE) && 
							raw[JSON_PROPERTY_ERROR].hasOwnProperty(JSON_PROPERTY_ERROR_MESSAGE))
						{
							// Creates the call error
							error = new CallError(CallErrorType.GENERIC, 
												  int(raw[JSON_PROPERTY_ERROR][JSON_PROPERTY_ERROR_CODE]),
												  String(raw[JSON_PROPERTY_ERROR][JSON_PROPERTY_ERROR_MESSAGE]));
							// Call > ERROR
							callError(error);
						}
						else
						{
							// ERROR > 'error' structure missing or not well formed
							formatError = true;
							formatErrorMessage = "'" + JSON_PROPERTY_ERROR + "' structure missing or not well formed";
						}
						break;
					}
						
					case Call.RESULT_SERVER_EXCEPTION:
					{
						// Check for error structure
						if (raw.hasOwnProperty(JSON_PROPERTY_ERROR) && 
							raw[JSON_PROPERTY_ERROR].hasOwnProperty(JSON_PROPERTY_ERROR_CODE) && 
							raw[JSON_PROPERTY_ERROR].hasOwnProperty(JSON_PROPERTY_ERROR_MESSAGE))
						{
							// Creates the call error
							error = new CallError(CallErrorType.SERVER_EXCEPTION, 
												  int(raw[JSON_PROPERTY_ERROR][JSON_PROPERTY_ERROR_CODE]),
												  String(raw[JSON_PROPERTY_ERROR][JSON_PROPERTY_ERROR_MESSAGE]));
							// Call > ERROR
							callError(error);
						}
						else
						{
							// ERROR > 'error' structure missing or not well formed
							formatError = true;
							formatErrorMessage = "'" + JSON_PROPERTY_ERROR + "' structure missing or not well formed";
						}
						break;
					}
						
					default:
					{
						// ERROR > 'result' field value unknown
						formatError = true;
						formatErrorMessage = "'" + JSON_PROPERTY_RESULT + "' field value (" + result + ") is unknown";
						break;
					}
				}
			}
			else
			{
				// ERROR > 'result' field missing
				formatError = true;
				formatErrorMessage = "'" + JSON_PROPERTY_RESULT + "' field missing";
			}
				
			
			// Checks if any format error happens
			if (formatError)
			{
				// Call > FAILED: response format is wrong
				callFailed(CallFailType.RESPONSE_ERROR, formatErrorMessage);
			}
			
		}
		
		protected function setStatus(newStatus:CallStatus):void
		{
			_status = newStatus;
			// dispatch the status change
			dispatchStatusChangedEvent();
		}
		
		// URLLoader event handlers
		
		protected function loaderOpen(e:Event):void
		{
			// Status EXECUTING
			setStatus(CallStatus.EXECUTING);
		}
		
		protected function loaderComplete(event:Event):void
		{
			// Common JSON responce parsing
			var raw:Object = null;
			// Decoding flag
			var parsing:Boolean = false;
			// Safe exec
			try
			{
				// Retrieve the results as text
				var result:String = String(_loader.data);
				// DEBUG > Received result from server
				if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "Call.serviceResultHandler() ' {0} result:  {1}", toUrl() , result);
				// JSON parsing
				try
				{
					// JSON Decoding
					raw = JsonUtils.decode(result);
					// Decoding succeded
					parsing = true;
				}
				catch (error:Error)
				{
					// JSON message
					var msg:String = "JSON parsing error: " + error.message + " - data: " + result;
					// Call > FAILED
					callFailed(CallFailType.JSON_ERROR, msg, error);
				}
				// Check parsing result
				if (parsing)
				{
					// Verify the handling
					if (_useAutoResultHandling)
					{
						parseResponce(raw);
					}
					else
					{
						// Call > SUCCEDED: without any data result parsing
						callSucceeded(raw);
					}
				}
			}
			// Unexpected exception
			catch (exc:Error)
			{
				callFailed(CallFailType.EXCEPTION, "Unexpected error in serviceResultHandler(): " + exc.message, exc);
			}
		}
		
		protected function loaderIOError(e:IOErrorEvent):void
		{
			// DEBUG > Received IO ERROR event
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "Call IO Error '{0}'", toUrl());
			// Safe exec
			try
			{
//				// Check retries
//				if (_retries < ERROR_RETRIES_IO)
//				{
//					_retries++;
//					// Log
//					if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "Call retry n° '{0}'", _retries);
//					// Executes the call
//					callExecute();
//				}
//				else
//				{
//					// CALL > IO error
//					callFailed(CallFailType.IO_ERROR, e.text, null);
//				}
				
				
				// Check the last HTTP_STATUS received to verify a retriable error
				var canRetry:Boolean = RETRY_HTTP_STATUS_LIST.indexOf(_httpStatus) > -1; 					
				// CALL > IO error
				callFailed(CallFailType.IO_ERROR, e.text, null, canRetry);
			}
			// Unexpected exception
			catch (exc:Error)
			{
				callFailed(CallFailType.EXCEPTION, "Unexpected error in loaderIOError(): " + exc.message, exc);
			}
		}
		
		protected function loaderSecurityError(e:SecurityErrorEvent):void
		{
			// DEBUG > Received FAULT from server
			if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "Call.loaderSecurityError() '{0}'", toUrl());
			// Safe exec
			try
			{
				// CALL > Security error
				callFailed(CallFailType.SECURITY_ERROR, e.text, null);
			}
			// Unexpected exception
			catch (exc:Error)
			{
				callFailed(CallFailType.EXCEPTION, "Unexpected error in loaderSecurityError(): " + exc.message, exc);
			}
		}
		
		protected function loaderHttpStatusChanged(e:HTTPStatusEvent):void
		{
			// Safe exec
			try
			{
				// Saves the http status
				_httpStatus = e.status;
				// Debugging
				if (Logger.canDebug(DEBUG_KEY_HTTP_STATUS)) 
				{
					// Verifica stato errato!
					if (_httpStatus != 200)
					{
						// LOG > Warning for not valid HTTP_STATUS code
						Logger.warn(Logger.format(LOG_MARKER_HTTP_STATUS + " : {0} - {1}", false), e.status, toUrl());
					}
				}
			}
			// Unexpected exception
			catch (exc:Error)
			{
				Logger.error("Call.loaderHttpStatusChanged() unexpected exception: {0}", exc, exc.message);
			}
		}
		
		// Call internal handlers
		
		protected function callExecute():void
		{
			// Create the URLVariables to pass to server with the request
			var variables:URLVariables = new URLVariables();
			for (var prop:String in _params)
			{
				variables[prop] = _params[prop];
			}
			// Add timestamp to avoid browser cache
			variables.nocache = (new Date()).time;
			// Creates the URLRequest
			var request:URLRequest = new URLRequest(Application.instance.getAbsoluteCallUrl(_url));
			request.method	= URLRequestMethod.GET;
			request.data 	= variables;
			// Execute the call
			_loader.load(request);
		}
		
		protected function callSucceeded(data:Object):void
		{
			if (_status == CallStatus.EXECUTING)
			{
				// Save the data returned
				_data = data;
				// STATUS > EXECUTED_WITH_SUCCESS
				setStatus(CallStatus.EXECUTED_WITH_SUCCESS);
				// Event dispatch
				dispatchEvent(new CallSuccessEvent(this));
			}
			else
			{
				Logger.warn("Call.callSucceeded() invoked when Call.status is no EXECUTING (status:{0})", _status);
			}
		}
		
		protected function callError(error:CallError):void
		{
			// Saves the error
			_error = error;
			// Call > ERROR
			setStatus(CallStatus.EXECUTED_WITH_ERROR);
			// Crete fail event
			var errorEvent:CallErrorEvent = new CallErrorEvent(this);
			// Call generic handling
			Call.globalCallError(errorEvent);
			// Event dispatch
			dispatchEvent(errorEvent);
		}
		
		protected function callFailed(type:CallFailType, message:String = null, error:Error = null, canRetry:Boolean = false):void
		{
			// Create an CallError to store fail informations
			_error = new CallError(CallErrorType.CALL_FAILED, type.id, message);
			// STATUS > FAILED
			setStatus(CallStatus.FAILED);
			// Crete fail event
			var failEvent:CallFailEvent = new CallFailEvent(this, type, message, error, canRetry);
			// Call generic handling
			Call.globalCallFailed(failEvent);
			// Event dispatch
			dispatchEvent(failEvent);
		}

		// Dump and encoding utils
		
		protected function encodeParams(urlEscape:Boolean = false):String
		{
			var dump:String = "";
			for (var p:String in _params)
			{
				if (dump != "") dump += "&";
				if (_params[p] is Array)
				{
					var a:Array = _params[p] as Array;
					dump += p + "=";
					for (var i:int = 0; i<a.length; i++)
					{
						if (i > 0) dump += ",";
						dump += urlEscape ? escape( a[i].toString() ) : a[i].toString();
					}
				}
				else
				{
					dump += p + "=" + ( urlEscape ? escape( _params[p].toString() ) : _params[p].toString() );
				}
			}
			return dump;
		}
		
		// Event dispatchers
		
		protected function dispatchStatusChangedEvent():void
		{
			dispatchEvent(new CallEvent(CallEvent.STATUS_CHANGED, this));
		}
		
		// -------------------------------------------------------------------------------
		// PUBLIC METHODS
		// -------------------------------------------------------------------------------
		
		/**
		 * Executes the call to the server 
		 */		
		public function execute():void
		{
			// Status check
			if (_status != CallStatus.READY)
				throw new Error("A Call is not ready to be executed! Call clear before to execute it again or cancel the current execution");
			// Send the HTTP request
			try
			{
				// DEBUG > Log URL and params before execute the call
				if (Logger.canDebug(DEBUG_KEY)) Logger.debug(LOG_MARKER + "Call.execute() '{0}' params: {1}", toUrl() ,JsonUtils.encode(_params));
				// Executes the call
				callExecute();
			}
			catch (e:Error)
			{
				// Call > FAILED
				callFailed(CallFailType.REQUEST_ERROR, "Call.execute() in error: " + e.message, e);
			}
		}
		
		/**
		 * Cancel the current call execution 
		 */		
		public function cancel():void
		{
			// Status check
			if (_status != CallStatus.EXECUTING) throw new Error("Call is not executing the request, cannot be cancelled!");
			// Cancels the call
			_loader.close();
			/*
			// Status check
			// if (_status != CallStatus.EXECUTING) throw new Error("Call is not executing the request, cannot be cancelled!");
			if (_status == CallStatus.EXECUTING)
			{
				// Cancels the call
				_loader.close();
			}
			// Clear the call
			clear();
			*/
		}
		
		/**
		 * Clears the current call, removing receved data, errors and setting the status to READY
		 */		
		public function clear():void
		{
			// if the call is running, stops response receiving
			if (_status == CallStatus.EXECUTING)
			{
				cancel();
			}
			// Data reset
			// _retries = 0;
			_httpStatus = INVALID_HTTP_STATUS;
			_data = null;
			_error = null;
			// status reset
			setStatus(CallStatus.READY);
		}
	
		public function equals(call:Call):Boolean
		{
			return	toUrl(false) == call.toUrl(false);
		}
		
		public function toUrl(urlEscape:Boolean = false):String
		{
			var dump:String = "";
			try
			{
				// dump += CallFactory.BASE_URL +_url;
				dump += Application.instance.config.callsUrl +_url;
				dump += "?";
				dump += encodeParams(urlEscape);
			} 
			catch(error:Error) 
			{
				dump += Application.instance.config.callsUrl + _url;
			}
			return dump;
		}
		
		override public function toString():String
		{
			return "Call '" + _url + "' (status: " + _status.name + ", params: " + JsonUtils.encode(_params) + ", error: " + JsonUtils.encode(_error) + ")";
		}
		
		
		// -------------------------------------------------------------------------------
		// STATIC METHODS
		// -------------------------------------------------------------------------------
		
		private static function globalCallFailed(event:CallFailEvent):void
		{
			if (!_enableGlobalCallFailedHandling) return;
			if (Call.hasGlobalCallFailedHandler) _globalCallFailedHandler(event);
		}
		
		private static function globalCallError(event:CallErrorEvent):void
		{
			if (!_enableGlobalErrorHandling) return;
			if (Call.hasGlobalCallErrorHandler) _globalCallErrorHandler(event);
		}
		
		public static function setGlobalCallFailedHandler(handler:Function):void
		{
			if (_globalCallFailedHandler != null) throw new Error("Global handler for CallFailedEvent already set");
			_globalCallFailedHandler = handler;
		}
		
		public static function setGlobalCallErrorHandler(handler:Function):void
		{
			if (_globalCallErrorHandler != null) throw new Error("Global handler for GenericErrorEvent already set");
			_globalCallErrorHandler = handler;			
		}

		public static function get hasGlobalCallFailedHandler():Boolean
		{
			return _globalCallFailedHandler != null;
		}
		
		public static function get hasGlobalCallErrorHandler():Boolean
		{
			return _globalCallErrorHandler != null;
		}
		
		public static function get enableGlobalCallFailedHandling():Boolean
		{
			return _enableGlobalCallFailedHandling;
		}
		
		public static function set enableGlobalCallFailedHandling(value:Boolean):void
		{
			_enableGlobalCallFailedHandling = value;
		}
		
		public static function get enableGlobalCallErrorHandling():Boolean
		{
			return _enableGlobalErrorHandling;
		}
		
		public static function set enableGlobalCallErrorHandling(value:Boolean):void
		{
			_enableGlobalErrorHandling = value;
		}
		
	}
}